配置文件搞不定的,就得依赖脚本。C++ 程序想嵌点脚本,Lua 几乎是首选。
Lua 的源码自带 Makefile
,可以编译出静态库、解释器、编译器三个目标文件,作为宿主的 C++ 程序,除了要包含 Lua 头文件,还应该链接这个静态库。
如果 C++ 程序是由 CMake 来构建的,那么用 CMake 为 Lua 创建一个静态库,也不是什么难事。CMake 很好的解决了跨平台的问题。
其实脚本扩展的问题只有两个:一、怎么让 Lua 访问 C++ 对象?二、怎么让 C++ 访问 Lua 对象?当然所谓对象,是个宽泛的概念,包括变量、函数、类,等等。
通过 LuaBridge,可以很方便的解决这两个问题。
头文件
先交代一下头文件,后面就不提了。
首先包含 Lua 的几个头文件,因为是 C 代码,放在 extern "C"
里才能跟 C++ 程序混编。
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
} // extern "C"
其次是 LuaBridge 头文件,LuaBridge 跟 STL 一样,只有头文件,直接包含使用。
#include "LuaBridge/LuaBridge.h"
Lua 访问 C++
函数
C++ 函数 SayHello
「导出」为 Lua 函数 sayHello
,然后通过 luaL_dostring
执行 Lua 代码,调用这个函数。
void SayHello() {
std::cout << "Hello, World!" << std::endl;
}
int main() {
lua_State* L = luaL_newstate();
luaL_openlibs(L);
luabridge::getGlobalNamespace(L)
.addFunction("sayHello", SayHello);
luaL_dostring(L, "sayHello()");
lua_close(L);
}
输出:
Hello, World!
为 SayHello
加个参数:
void SayHello(const char* to) {
std::cout << "Hello, " << to << "!" << std::endl;
}
luabridge::getGlobalNamespace(L)
.addFunction("sayHello", SayHello);
luaL_dostring(L, "sayHello('Lua')");
输出:
Hello, Lua!
类
C++ 的类导出为 Lua 的表,类的成员函数对应于表的成员。假如有一个类 Line
,表示文本文件中的一行:
class Line {
public:
Line(const std::string& data)
: data_(data) {
}
size_t Length() const {
return data_.length();
}
private:
std::string data_;
};
构造函数
导出构造函数用 addConstructor
,导出成员函数还是用 addFunction
:
luabridge::getGlobalNamespace(L)
.beginClass<Line>("Line")
.addConstructor<void(*)(const std::string&)>()
.addFunction("getLength", &Line::Length)
.endClass();
构造函数无法取址,调用 addConstructor
时需传递模板参数以指明类型。
测试:
const char* str =
"line = Line('test')\n"
"print(line:getLength())\n";
luaL_dostring(L, str);
输出:
4
如果有多个构造函数,比如还有一个缺省构造函数:
Line::Line();
则只能导出一个。下面这种写法,第二个会覆盖第一个:
luabridge::getGlobalNamespace(L)
.beginClass<Line>("Line")
.addConstructor<void(*)(void)>() // 被下一句覆盖
.addConstructor<void(*)(const std::string&)>()
.endClass();
你不可能让同一个名字指代两件事情。
成员函数
考虑一个稍微复杂的成员函数,StartWith
判断一行文本是否以某个字符串打头,参数 ignore_spaces
决定是否忽略行首的空格。对实现不感兴趣的可以完全忽略。
bool Line::StartWith(const std::string& str,
bool ignore_spaces) const {
size_t i = 0;
if (ignore_spaces && !IsSpace(str[0])) {
for (; i < data_.size() && IsSpace(data_[i]); ++i) {
}
}
if (data_.size() < i + str.size()) {
return false;
}
if (strncmp(&data_[i], &str[0], str.size()) == 0) {
return true;
}
return false;
}
通过 addFunction
导出到 Lua:
addFunction("startWith", &Line::StartWith)
测试:
const char* str =
"line = Line(' if ...')\n"
"print(line:startWith('if', false))\n"
"print(line:startWith('if', true))\n";
输出:
false
true
输出参数
现在为 StartWith
添加可选的输出参数,以便 ignore_spaces
为 true
时能够返回偏移信息(第一个非空字符的下标):
bool Line::StartWith(const std::string& str,
bool ignore_spaces,
int* off = NULL) const {
size_t i = 0;
if (ignore_spaces && !IsSpace(str[0])) {
for (; i < data_.size() && IsSpace(data_[i]); ++i) {
}
}
if (data_.size() < i + str.size()) {
return false;
}
if (strncmp(&data_[i], &str[0], str.size()) == 0) {
if (off != NULL) {
*off = static_cast<int>(i);
}
return true;
}
return false;
}
输出参数在 C/C++ 里是很常见的用法,可以让一个函数返回多个值。但是用 addFunction
导出的 StartWith
并不能被 Lua 调用,因为 Lua 没有输出参数。幸运的是,Lua 的函数可以有多个返回值,为了让 StartWith
返回多个值,我们得做一层 Lua CFunction
的包装。
// Lua CFunction wrapper for StartWith.
int Line::Lua_StartWith(lua_State* L) {
// 获取参数个数
int n = lua_gettop(L);
// 验证参数个数
if (n != 3) {
luaL_error(L, "incorrect argument number");
}
// 验证参数类型
if (!lua_isstring(L, 2) || !lua_isboolean(L, 3)) {
luaL_error(L, "incorrect argument type");
}
// 获取参数
std::string str(lua_tostring(L, 2));
bool ignore_spaces = lua_toboolean(L, 3) != 0;
// 转调 StartWith
int off = 0;
bool result = StartWith(str, ignore_spaces, &off);
// 返回结果
luabridge::push(L, result);
luabridge::push(L, off);
return 2; // 返回值有两个
}
类型为 int (*) (lua_State*)
的函数就叫 Lua CFunction
。改用 addCFunction
导出 Lua_StartWith
:
addCFunction("startWith", &Line::Lua_StartWith)
测试:
const char* str =
"line = Line(' if ...')\n"
"ok, off = line:startWith('if', true)\n"
"print(ok, off)\n";
输出:
true 2
变参
既然已经做了 CFunction
的封装,不如做得更彻底一些。鉴于 Lua 对变参的良好支持,我们让 startWith
支持变参,比如既可以判断是否以 'if'
打头:
line:startWith(true, 'if')
也可以判断是否以 'if'
或 'else'
打头:
line:startWith(true, 'if', 'else')
为此,ignore_spaces
变成了第一个参数,后面是字符串类型的变参,具体实现如下:
int Line::Lua_StartWith(lua_State* L) {
int n = lua_gettop(L);
if (n < 3) {
luaL_error(L, "incorrect argument number");
}
if (!lua_isboolean(L, 2)) {
luaL_error(L, "incorrect argument type");
}
bool ignore_spaces = lua_toboolean(L, 2) != 0;
bool result = false;
int off = 0;
// 逐个比较字符串变参,一旦匹配就跳出循环。
for (int i = 3; i <= n; ++i) {
if (!lua_isstring(L, i)) {
break;
}
std::string str(lua_tostring(L, i));
if (StartWith(str, ignore_spaces, &off)) {
result = true;
break;
}
}
luabridge::push(L, result);
luabridge::push(L, off);
return 2;
}
测试:
const char* str =
"line = Line(' else ...')\n"
"ok, off = line:startWith(true, 'if', 'else')\n"
"print(ok, off)\n";
输出:
true 2
执行 Lua 文件
前面示例执行 Lua 代码全部使用 luaL_dostring
,实际项目中,Lua 代码主要以文件形式存在,就需要 luaL_dofile
。
测试:
luaL_dofile(L, "test.lua);
文件 test.lua
的内容为:
line = Line(' else ...')
ok, off = line:startWith(true, 'if', 'else')
print(ok, off)
输出:
true 2
C++ 访问 Lua
通过 getGlobal
函数可以拿到「全局」的 Lua 对象,类型为 LuaRef
。
int main() {
lua_State* L = luaL_newstate();
luaL_openlibs(L);
{ // 为了让 LuaRef 对象在 lua_close(L) 之前析构
const char* str =
"world = 'World'\n"
"sayHello = function(to)\n"
" print('Hello, ' .. to .. '!')\n"
"end\n";
luaL_dostring(L, str);
using namespace luabridge;
LuaRef world = getGlobal(L, "world");
LuaRef say_hello = getGlobal(L, "sayHello");
say_hello(world.cast<const char*>());
}
lua_close(L);
}
输出:
Hello, World!
字符串
Lua 没有字符类型,也没有 Unicode
字符串(特指 wchar_t*
)。
bool IsSpace(char c) {
return c == ' ' || c == '\t';
}
luabridge::getGlobalNamespace(L)
.addFunction("isSpace", IsSpace);
luaL_dostring(L, "print(isSpace(' '))");
luaL_dostring(L, "print(isSpace(' '))");
luaL_dostring(L, "print(isSpace('c'))");
输出:
true
true
false
如果 IsSpace
参数为 wchar_t
:
bool IsSpace(wchar_t c) {
return c == L' ' || c == L'\t';
}
在 Lua 里调用 isSpace(' ')
时,LuaBridge 便会断言失败:
Assertion failed: lua_istable (L, -1), file e:\proj\lua_test\third_party\include\luabridge\detail/Us
erdata.h, line 189
折中的办法是,为 IsSpace(wchar_t c)
提供一个 wrapper,专供 Lua 使用。
bool Lua_IsSpace(char c) {
return IsSpace((wchar_t)c);
}
luabridge::getGlobalNamespace(L)
.addFunction("isSpace", Lua_IsSpace);
当然前提是,Lua 代码调用 isSpace
时,只会传入 ASCII 字符。
错误处理
为了方便问题诊断和错误处理,有必要为内置的函数或宏做一些封装。
luaL_dostring
bool DoLuaString(lua_State* L,
const std::string& str,
std::string* error = NULL) {
if (luaL_dostring(L, str.c_str()) != LUA_OK) {
if (error != NULL) {
// 从栈顶获取错误消息。
if (lua_gettop(L) != 0) {
*error = lua_tostring(L, -1);
}
}
return false;
}
return true;
}
测试:故意调用一个不存在的函数 SayHello
(应该是 sayHello
)。
std::string error;
if (!DoLuaString(L, "SayHello('Lua')", &error)) {
std::cerr << error << std::endl;
}
输出(试图调用一个空值):
[string "SayHello('Lua')"]:1: attempt to call a nil value (global 'SayHello')
luaL_dofile
与 luaL_dostring
的封装类似。
bool DoLuaFile(lua_State* L,
const std::string& file,
std::string* error = NULL) {
if (luaL_dofile(L, file.c_str()) != LUA_OK) {
if (error != NULL) {
// 从栈顶获取错误消息。
if (lua_gettop(L) != 0) {
*error = lua_tostring(L, -1);
}
}
return false;
}
return true;
}
luabridge::LuaRef
LuaRef world = getGlobal(L, "world");
if (!world.isNil() && world.isString()) {
// ...
}
LuaRef say_hello = getGlobal(L, "sayHello");
if (!say_hello.isNil() && say_hello.isFunction()) {
// ...
}
luabridge::LuaException
如果 Lua 代码有什么问题,LuaBridge 会引发 LuaException
异常,相关代码最好放在 try...catch
中。
try {
// ...
} catch (const luabridge::LuaException& e) {
std::cerr << e.what() << std::endl;
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。